import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;

/**
 * Dijkstar's modified algorithm: Based on the given input metric and the speed of the vehicle
 * This algorithm prints the vehicles following details:
 * source
 * destination
 * route traversed
 * and time taken to reach the destination from source
 * Updates the traffic for the vehicle without considering the maximum vehicles in tht road
 * @author Prarthana
 *
 */

/**
 * Class to hold the time related information related to the vehicle
 */

public class DijkstraFinalUpdateTraffic {

	private final List<Road> roads;
	private static int vehicleSize =0;
	private long min = (long) 60.00;
	Timer timer;
	long startTime, endTime;
	private List<Vehicle> vehicles;
	private Map<Vehicle, Set<Intersection>> vehicleSettledNodes;
	private Map<Vehicle, Set<Intersection>> vehicleUnSettledNodes;
	private Map<Vehicle, Map<Intersection, VehicleCurInterSectDir>> vehiclePredecessors;
	private Map<Vehicle, Intersection> vehicleCurrentSource;
	private Map<Vehicle, VehicleTime> vehicleTimeCalc;
	private Map<Vehicle, Map<Intersection, Double>> vehicleMetric;
	private Map<Vehicle, Boolean> vehicleStatus;
	private Map<Vehicle, VehicleCurInterSectDir> vehicleCurStatus;
	private final RoadMap graph;
	double factor = 1e3;
	int count = 0;
	private static final Object OBJ_LOCK = new Object(); //better
	FileWriter fstream, f1stream;
	BufferedWriter out, out1;
	
	/**
	 * Construction class
	 * @param graph
	 * @throws IOException 
	 */
	public DijkstraFinalUpdateTraffic(RoadMap graph) throws IOException {
		vehicles = new ArrayList<Vehicle>();
		this.roads = new ArrayList<Road>(graph.getRoads().values());
		vehicleSettledNodes = new HashMap<Vehicle, Set<Intersection>>();
		vehicleUnSettledNodes = new HashMap<Vehicle, Set<Intersection>>();
		vehicleMetric = new HashMap<Vehicle, Map<Intersection, Double>>();
		vehiclePredecessors = new HashMap<Vehicle, Map<Intersection, VehicleCurInterSectDir>>();
		vehicleTimeCalc = new HashMap<Vehicle, VehicleTime>();
		vehicleCurrentSource = new HashMap<Vehicle, Intersection>();
		vehicleStatus = new HashMap<Vehicle, Boolean>();
		vehicleCurStatus = new HashMap<Vehicle, VehicleCurInterSectDir>();
		this.graph = graph;
		fstream = new FileWriter("test.csv");
		out = new BufferedWriter(fstream);
		f1stream = new FileWriter("test1.csv");
		out1 = new BufferedWriter(f1stream);
		initialize();
	}
	
	/**
	 * Initializing the variables for the algorithm
	 */
	public void initialize(){
		vehicles = graph.getVehicles();
		vehicleSize = vehicles.size();
		
		for(Vehicle vehicle:vehicles){
			HashSet<Intersection> settledNodes = new HashSet<Intersection>();
			vehicleSettledNodes.put(vehicle, settledNodes);
			HashMap<Intersection, Double> distance = new HashMap<Intersection, Double>();
			Intersection source = vehicle.getOrigin();
			distance.put(source, 0.0);
			vehicleMetric.put(vehicle, distance);
			HashSet<Intersection> unSettledNodes = new HashSet<Intersection>();
			unSettledNodes.add(source);
			vehicleUnSettledNodes.put(vehicle, unSettledNodes);
			HashMap<Intersection, VehicleCurInterSectDir> predecessors = new HashMap<Intersection, VehicleCurInterSectDir>();
			vehiclePredecessors.put(vehicle, predecessors);
			vehicleCurrentSource.put(vehicle, source);
			vehicleStatus.put(vehicle, false);
		}		
		startTime = System.currentTimeMillis();
		timer = new Timer();
	    timer.schedule(new IntersectionTask(), 0, 1);
	}
	
	/**
	 * Timer task class to be called every 1 time interval
	 * @author Prarthana
	 *
	 */
	class IntersectionTask extends TimerTask{
		public synchronized void run(){
			count++;
			int completedVehicles =0;
			
			//Updating the section after the calculations
			for(Vehicle vehicle:vehicles){
				if(!vehicleStatus.get(vehicle)){
					VehicleCurInterSectDir vehicleSource = vehicleCurStatus.get(vehicle);
					if(vehicleSource!=null && vehicleSource.curSect != null &&  vehicleSource.curDir!=null)
						vehicle.updateCurrentRoadwithoutMax(vehicleSource.curSect, vehicleSource.curDir);
				}
			}
			for(Vehicle vehicle:vehicles){
				boolean status = vehicleStatus.get(vehicle);
				if(!status){
					VehicleTime time = vehicleTimeCalc.get(vehicle);
					if(time!=null){
						//Call the execute function if the time elapsed is greater than the time to travel the section
						if(time.timeElapsed >= time.timeleft){
							time.timeleft = 0;
							time.timeElapsed = 0;
							try {
								actualNextHop(vehicle);
							} catch (IOException e) {
								// TODO Auto-generated catch block
								e.printStackTrace();
							}
						}else{
							time.timeElapsed+= 20;//60*1000; //This increases by min level
						}
						vehicleTimeCalc.put(vehicle, time);
					}else{
						//Initial case
						VehicleTime newTime = new VehicleTime();
						newTime.timeleft = 0;
						newTime.timeElapsed = 0;
						vehicleTimeCalc.put(vehicle, newTime);
						try {
							actualNextHop(vehicle);
						} catch (IOException e) {
							// TODO Auto-generated catch block
							e.printStackTrace();
						}
					}
				}else{
					completedVehicles++;
				}
			}
			
			
			if(vehicleSize == completedVehicles){
				System.out.println("::::::::::::::::::::::::::::::::::::::::::::DONE:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::");
				timer.cancel();
		         timer.purge();
		         try {
					out.close();
				} catch (IOException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
		         try {
					out1.close();
				} catch (IOException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
		         return;
			}
		}

	}
	
	/**
	 * Function which calculates the next intersection for the vehicle
	 * @param vehicle
	 */
	public synchronized LinkedList<VehicleCurInterSectDir> execute(Vehicle vehicle)  {
		Intersection destination = vehicle.getDestination();
		Set<Intersection> unSettledNodes = vehicleUnSettledNodes.get(vehicle);
		Set<Intersection> settledNodes = vehicleSettledNodes.get(vehicle);
		//if(vehicleCurrentSource.get(vehicle) == destination) return null;
		while (unSettledNodes.size() > 0) {
			//ArrayList<Intersection> nodes = getMinimum(unSettledNodes, vehicle);
			Intersection node = getMin(unSettledNodes, vehicle);
			settledNodes.add(node);
			unSettledNodes.remove(node);
			vehicleUnSettledNodes.put(vehicle, unSettledNodes);
			vehicleSettledNodes.put(vehicle, settledNodes);
			if(destination != node){
				findMinimalDistances(node, vehicle);
			}else{
				return getPath(vehicle.getDestination(), vehicle);
			}
		}
		return null;
				
	}
	
	private synchronized void actualNextHop(Vehicle vehicle) throws IOException{
		Intersection destination = vehicle.getDestination();
		
		//clear all the variables for vehicle
		Set<Intersection> settledNodes = vehicleSettledNodes.get(vehicle);
		settledNodes.clear();
		vehicleSettledNodes.put(vehicle, settledNodes);
		Map<Intersection, Double> distance = vehicleMetric.get(vehicle);
		distance.clear();
		Intersection currentSource = vehicleCurrentSource.get(vehicle);
		distance.put(currentSource, 0.0);
		vehicleMetric.put(vehicle, distance);
		Set<Intersection> unSettledNodes = vehicleUnSettledNodes.get(vehicle);
		unSettledNodes.clear();
		unSettledNodes.add(currentSource);
		vehicleUnSettledNodes.put(vehicle, unSettledNodes);
		Map<Intersection, VehicleCurInterSectDir> predecessors = vehiclePredecessors.get(vehicle);
		predecessors.clear();
		vehiclePredecessors.put(vehicle, predecessors);
		
		//Call the dijkstra's for next intersection
		
			LinkedList<VehicleCurInterSectDir> path = execute(vehicle);
			if(path == null){
				if(!vehicleStatus.get(vehicle))
					printDetails(vehicle);
				return;
			}
			VehicleCurInterSectDir nextDetail;
			Intersection nextIntersection;
			nextDetail = path.get(0);
			nextIntersection = path.get(1).curIntersection;
				//update source
				vehicleCurrentSource.put(vehicle, nextIntersection);
				VehicleCurInterSectDir curStatus = new VehicleCurInterSectDir();
				curStatus.curDir = nextDetail.curDir;
				curStatus.curSect = nextDetail.curSect;
				curStatus.curIntersection = nextIntersection;
				vehicleCurStatus.put(vehicle, curStatus);
				VehicleTime time = vehicleTimeCalc.get(vehicle);
				long dist = (new Double(nextDetail.curSect.getDistance()*min*min)).longValue();
				double sectionWeight = nextDetail.curSect.getWeight(nextDetail.curDir);
				Metric metric = graph.getMetric();
				double idealWeight = metric.calculateWeight(nextDetail.curSect.getDistance(), 1);
				double speed = vehicle.getSpeed();
				double ratio = Math.abs(1/sectionWeight);
				double decSpeed = 15 + ((speed-15) *ratio);
				System.out.println("Speed"+speed+"decspeed"+decSpeed+"ratio"+ratio);
				long newVehicleSpeed = (new Double(decSpeed)).longValue();
				
				time.timeleft = (dist*(long)factor/newVehicleSpeed)/(long)factor; 
				System.out.println("timeLeft"+time.timeleft);
				time.timeElapsed = 0;
				vehicleTimeCalc.put(vehicle, time);
				if(nextIntersection == destination){
					//The vehicle has found the destination, stop the execution for the vehicle
					//System.out.println("Coming here");
					printDetails(vehicle);
				}
		
	
	}
	
	
	private synchronized void printDetails(Vehicle vehicle) throws IOException{
		endTime = System.currentTimeMillis();
		Set<Intersection> unSettledNodes = vehicleUnSettledNodes.get(vehicle);
		unSettledNodes.clear();
		vehicleUnSettledNodes.put(vehicle, unSettledNodes);
//		System.out.println("-----------------------------------New Vehicle--------------------------------------------------");
//		System.out.println("Vehicle path  "+ vehicle.getName());
//		System.out.println("Vehicle source "+ vehicle.getOrigin());
//		System.out.println("Vehicle destination  "+":"+vehicle.getDestination());
		//print the path
		//System.out.println("Path taken by the vehicle  "+":\n"+vehicle.printPath());
		LinkedList<String> list = vehicle.printPath();
		System.out.println("Path taken by the vehicle  "+":\n"+list);
		long algoTime = (endTime - startTime);
		int size = 0;
		if(!list.isEmpty()) size = vehicle.printPath().size();
		System.out.println("size"+size);
		out.write(size+","+count+"\n");
		out1.write(size+","+algoTime+"\n");
//		System.out.println("Total Time Taken for car:"+count+"min");
//		System.out.println("Time Taken for algorithm:"+(endTime - startTime)+"millisec");
//		System.out.println("-----------------------------------End Vehicle--------------------------------------------------");
		vehicleStatus.put(vehicle, true);
		vehicle.updateCurrentRoad(null, null);
		//decrementCount();		
	}
	
	public static synchronized void decrementCount() {
		synchronized(OBJ_LOCK) {
			vehicleSize--;
		}
    }

	/**
	 * Find the minimum distance neighbours for a particular intersection
	 * @param node
	 * @param vehicle
	 */
	private synchronized void findMinimalDistances(Intersection node, Vehicle vehicle) {
		HashMap<Intersection, Direction> adjacentNodes = getNeighbors(node, vehicle);
		for (Intersection target : adjacentNodes.keySet()) {
			Road nextRoad = getRoad(target, node);
			Section nextSection = getSection(target, node, nextRoad);
			double leftVal = Math.round(getShortestMetric(target, vehicle)*factor)/factor;
			double rightVal = Math.round((getShortestMetric(node, vehicle)	+ getMetric(nextSection, adjacentNodes.get(target)))*factor)/factor;
			if (Double.compare(leftVal,rightVal)>=0) {
					//Success adding the current section to the vehicle path
					Map<Intersection, Double> distance = vehicleMetric.get(vehicle);
					if(Double.compare(leftVal,rightVal)>0){
						distance.put(target, getShortestMetric(node, vehicle)
							+ getMetric(nextSection, adjacentNodes.get(target)));
					}
					vehicleMetric.put(vehicle, distance);
					Map<Intersection, VehicleCurInterSectDir> predecessors = vehiclePredecessors.get(vehicle);
					VehicleCurInterSectDir cur = new VehicleCurInterSectDir();
					cur.curDir = adjacentNodes.get(target);
					cur.curIntersection = node;
					cur.curSect = nextSection;
					predecessors.put(target, cur);
					vehiclePredecessors.put(vehicle, predecessors);
					Set<Intersection> unSettledNodes = vehicleUnSettledNodes.get(vehicle);
					unSettledNodes.add(target);
					vehicleUnSettledNodes.put(vehicle, unSettledNodes);
			}
		}
	}
	
	/**
	 * Find the Road consisting the current and next intersection
	 * @param node
	 * @param target
	 * @return
	 */
	private synchronized Road getRoad(Intersection node, Intersection target){
		for (Road edge : roads) {
			ArrayList<Intersection> intersections = new ArrayList<Intersection>(Arrays.asList(edge.getIntersections()));
			if (intersections.contains(node)
					&& intersections.contains(target)) {
				return edge;
			}
		}
		throw new RuntimeException("Should not happen");
	}
	
	/**
	 * Find the section consisting the current and next intersection
	 * @param node
	 * @param target
	 * @param road
	 * @return
	 */
	private synchronized Section getSection(Intersection node, Intersection target, Road road){
		Section[] sections = road.getSections();
		for (Section edge : sections) {
			ArrayList<Intersection> intersections = new ArrayList<Intersection>(edge.getIntersections());
			if (intersections.contains(node)
					&& intersections.contains(target)) {
				return edge;
			}
		}
		throw new RuntimeException("Should not happen");
	}

	/**
	 * get metric for the current section in the given direction
	 * @param section
	 * @param direction
	 * @return
	 */
	private synchronized double getMetric(Section section, Direction direction) {
		return section.getWeight(direction);
	}

	/**
	 * Get neighbors for a particular intersection for a particular direction
	 * @param node
	 * @param vehicle
	 * @return
	 */
	private synchronized HashMap<Intersection, Direction> getNeighbors(Intersection node, Vehicle vehicle) {
		HashMap<Intersection, Direction> neighbors = new HashMap<Intersection, Direction>();
		Section[] sections = node.getRoadSections(null);
		for (Section section : sections) {
			Intersection destination;
				destination = section.nextIntersection(Direction.North);
				if(destination!=null && !isSettled(destination, vehicle))
						neighbors.put(destination, Direction.North);
				destination = section.nextIntersection(Direction.South);
				if(destination!=null && !isSettled(destination, vehicle))
						neighbors.put(destination, Direction.South);
				destination = section.nextIntersection(Direction.East);
				if(destination!=null && !isSettled(destination, vehicle))
						neighbors.put(destination, Direction.East);
				destination = section.nextIntersection(Direction.West);
				if(destination!=null && !isSettled(destination, vehicle))
						neighbors.put(destination, Direction.West);
		}
		return neighbors;
	}

	/**
	 * Get the intersections with minimum metrices
	 * @param vertexes
	 * @param vehicle
	 * @return
	 */
	private synchronized ArrayList<Intersection> getMinimum(Set<Intersection> vertexes, Vehicle vehicle) {
		ArrayList<Intersection> minimum = new ArrayList<Intersection>();
		Intersection min = null;
		
		for (Intersection vertex : vertexes) {
			if (min == null) {
				min =  vertex;
				minimum.add(vertex);
			} else {
				if (getShortestMetric(vertex, vehicle) < getShortestMetric(min, vehicle)) {
					
					min = vertex;
					minimum.clear();
					minimum.add(vertex);
				}else 	if(getShortestMetric(vertex, vehicle) == getShortestMetric(min, vehicle)){
					minimum.add(vertex);
				}
			}
		}
		return minimum;
	}
	
	private synchronized Intersection getMin(Set<Intersection> vertexes, Vehicle vehicle) {
		Intersection minimum = null;
		for (Intersection vertex : vertexes) {
			if (minimum == null) {
				minimum = vertex;
			} else {
				if (getShortestMetric(vertex, vehicle) < getShortestMetric(minimum, vehicle)) {
					minimum = vertex;
				}
			}
		}
		return minimum;
	}

	/**
	 * Check whether the intersection is already traversed for the vehicle
	 * @param vertex
	 * @param vehicle
	 * @return
	 */
	private synchronized boolean isSettled(Intersection vertex, Vehicle vehicle) {
		Set<Intersection> settledNodes = vehicleSettledNodes.get(vehicle);
		return settledNodes.contains(vertex);
	}

	/**
	 * Find the shortest metric for a vehicle for a given intersection
	 * @param destination
	 * @param vehicle
	 * @return
	 */
	private synchronized Double getShortestMetric(Intersection destination, Vehicle vehicle) {
		Map<Intersection, Double> metric = vehicleMetric.get(vehicle);
		Double d = metric.get(destination);
		if (d == null) {
			return Double.MAX_VALUE;
		} else {
			return d;
		}
	}

	/*
	 * This method returns the path from the source to the selected target and
	 * NULL if no path exists
	 */
	public synchronized LinkedList<VehicleCurInterSectDir> getPath(Intersection target, Vehicle vehicle) {
		LinkedList<VehicleCurInterSectDir> path = new LinkedList<VehicleCurInterSectDir>();
		Intersection step = target;
		Map<Intersection, VehicleCurInterSectDir> predecessors = vehiclePredecessors.get(vehicle);
		// Check if a path exists
		if (predecessors.get(step) == null) {
			return null;
		}
		VehicleCurInterSectDir targetMap = new VehicleCurInterSectDir();
		targetMap.curIntersection = step;
		path.add(targetMap);
		while (predecessors.get(step) != null) {
			VehicleCurInterSectDir cur = predecessors.get(step);
			VehicleCurInterSectDir newNode = new VehicleCurInterSectDir();
			newNode.curDir = cur.curDir;
			newNode.curIntersection = cur.curIntersection;
			newNode.curSect = cur.curSect;
			step = cur.curIntersection;
			path.add(newNode);
		}
		// Put it into the correct order
		Collections.reverse(path);
		return path;
	}
	

}